/*
 *  Licensed to the Apache Software Foundation (ASF) under one
 *  or more contributor license agreements.  See the NOTICE file
 *  distributed with this work for additional information
 *  regarding copyright ownership.  The ASF licenses this file
 *  to you under the Apache License, Version 2.0 (the
 *  "License"); you may not use this file except in compliance
 *  with the License.  You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing,
 *  software distributed under the License is distributed on an
 *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 *  KIND, either express or implied.  See the License for the
 *  specific language governing permissions and limitations
 *  under the License.
 */
package org.codehaus.groovy.control;

import groovy.lang.GroovyClassLoader;
import groovy.lang.GroovyRuntimeException;
import groovy.transform.CompilationUnitAware;
import org.codehaus.groovy.GroovyBugError;
import org.codehaus.groovy.ast.ASTNode;
import org.codehaus.groovy.ast.ClassHelper;
import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.ast.CompileUnit;
import org.codehaus.groovy.ast.InnerClassNode;
import org.codehaus.groovy.ast.ModuleNode;
import org.codehaus.groovy.classgen.AsmClassGenerator;
import org.codehaus.groovy.classgen.ClassCompletionVerifier;
import org.codehaus.groovy.classgen.EnumCompletionVisitor;
import org.codehaus.groovy.classgen.EnumVisitor;
import org.codehaus.groovy.classgen.ExtendedVerifier;
import org.codehaus.groovy.classgen.GeneratorContext;
import org.codehaus.groovy.classgen.InnerClassCompletionVisitor;
import org.codehaus.groovy.classgen.InnerClassVisitor;
import org.codehaus.groovy.classgen.VariableScopeVisitor;
import org.codehaus.groovy.classgen.Verifier;
import org.codehaus.groovy.control.customizers.CompilationCustomizer;
import org.codehaus.groovy.control.io.InputStreamReaderSource;
import org.codehaus.groovy.control.io.ReaderSource;
import org.codehaus.groovy.control.messages.ExceptionMessage;
import org.codehaus.groovy.control.messages.Message;
import org.codehaus.groovy.control.messages.SimpleMessage;
import org.codehaus.groovy.syntax.SyntaxException;
import org.codehaus.groovy.tools.GroovyClass;
import org.codehaus.groovy.transform.ASTTransformationVisitor;
import org.codehaus.groovy.transform.AnnotationCollectorTransform;
import org.codehaus.groovy.transform.sc.StaticCompilationMetadataKeys;
import org.codehaus.groovy.transform.trait.TraitComposer;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;

import javax.tools.JavaFileObject;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.security.CodeSource;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * The CompilationUnit collects all compilation data as it is generated by the compiler system.
 * You can use this object to add additional source units to the compilation, or force the
 * compilation to be run again (to affect only the deltas).
 * <p>
 * You can also add PhaseOperations to this compilation using the addPhaseOperation method.
 * This is commonly used when you want to wire a new AST Transformation into the compilation.
 */
public class CompilationUnit extends ProcessingUnit {

    //---------------------------------------------------------------------------
    // CONSTRUCTION AND SUCH

    protected ASTTransformationsContext astTransformationsContext; // AST transformations state data

    protected Map<String, SourceUnit> sources;    // The SourceUnits from which this unit is built
    protected Map summariesBySourceName;      // Summary of each SourceUnit
    protected Map summariesByPublicClassName;       // Summary of each SourceUnit
    protected Map classSourcesByPublicClassName;    // Summary of each Class
    protected LinkedList<SourceUnit> queuedSources;

    protected CompileUnit ast;        // The overall AST for this CompilationUnit.
    protected List<GroovyClass> generatedClasses;  // The classes generated during classgen.

    protected Verifier verifier;   // For use by verify().

    protected boolean debug;      // Controls behavior of classgen() and other routines.
    protected boolean configured; // Set true after the first configure() operation

    protected ClassgenCallback classgenCallback;  // A callback for use during classgen()
    protected ProgressCallback progressCallback;  // A callback for use during compile()
    protected ResolveVisitor resolveVisitor;
    protected StaticImportVisitor staticImportVisitor;
    protected OptimizerVisitor optimizer;
    protected ClassNodeResolver classNodeResolver;

    LinkedList[] phaseOperations;
    LinkedList[] newPhaseOperations;

    private Set<JavaFileObject> javaCompilationUnitSet = new HashSet<>();

    /**
     * Initializes the CompilationUnit with defaults.
     */
    public CompilationUnit() {
        this(null, null, null);
    }

    /**
     * Initializes the CompilationUnit with defaults except for class loader.
     */
    public CompilationUnit(GroovyClassLoader loader) {
        this(null, null, loader);
    }

    /**
     * Initializes the CompilationUnit with no security considerations.
     */
    public CompilationUnit(CompilerConfiguration configuration) {
        this(configuration, null, null);
    }

    /**
     * Initializes the CompilationUnit with a CodeSource for controlling
     * security stuff and a class loader for loading classes.
     */
    public CompilationUnit(CompilerConfiguration configuration, CodeSource security, GroovyClassLoader loader) {
        this(configuration, security, loader, null);
    }

    /**
     * Initializes the CompilationUnit with a CodeSource for controlling
     * security stuff, a class loader for loading classes, and a class
     * loader for loading AST transformations.
     * <b>Note</b> The transform loader must be
     * able to load compiler classes. That means CompilationUnit.class.classLoader
     * must be at last a parent to transformLoader. The other loader has no such constraint.
     *
     * @param transformLoader - the loader for transforms
     * @param loader          - loader used to resolve classes against during compilation
     * @param security        - security setting for the compilation
     * @param configuration   - compilation configuration
     */
    public CompilationUnit(CompilerConfiguration configuration, CodeSource security,
                           GroovyClassLoader loader, GroovyClassLoader transformLoader) {
        super(configuration, loader, null);

        this.astTransformationsContext = new ASTTransformationsContext(this, transformLoader);
        this.queuedSources = new LinkedList<>();
        this.sources = new LinkedHashMap<>();
        this.summariesBySourceName = new HashMap<>();
        this.summariesByPublicClassName = new HashMap<>();
        this.classSourcesByPublicClassName = new HashMap<>();

        this.ast = new CompileUnit(this.classLoader, security, this.configuration);
        this.generatedClasses = new ArrayList<>();

        this.verifier = new Verifier();
        this.resolveVisitor = new ResolveVisitor(this);
        this.staticImportVisitor = new StaticImportVisitor();
        this.optimizer = new OptimizerVisitor(this);

        initPhaseOperations();
        addPhaseOperations();

        applyCompilationCustomizers();

        this.classgenCallback = null;
        this.classNodeResolver = new ClassNodeResolver();
    }

    private void initPhaseOperations() {
        int cnt = Phases.ALL + 1;
        phaseOperations = new LinkedList[cnt];
        newPhaseOperations = new LinkedList[cnt];
        for (int i = 0; i < phaseOperations.length; i++) {
            phaseOperations[i] = new LinkedList<>();
            newPhaseOperations[i] = new LinkedList<>();
        }
    }

    private void addPhaseOperations() {
        addPhaseOperation(new SourceUnitOperation() {
            @Override
            public void call(SourceUnit source) throws CompilationFailedException {
                source.parse();
            }
        }, Phases.PARSING);
        addPhaseOperation(convert, Phases.CONVERSION);
        addPhaseOperation(new PrimaryClassNodeOperation() {
            @Override
            public void call(SourceUnit source, GeneratorContext context,
                             ClassNode classNode) throws CompilationFailedException {
                EnumVisitor ev = new EnumVisitor(CompilationUnit.this, source);
                ev.visitClass(classNode);
            }
        }, Phases.CONVERSION);
        addPhaseOperation(resolve, Phases.SEMANTIC_ANALYSIS);
        addPhaseOperation(staticImport, Phases.SEMANTIC_ANALYSIS);
        addPhaseOperation(new PrimaryClassNodeOperation() {
            @Override
            public void call(SourceUnit source, GeneratorContext context,
                             ClassNode classNode) throws CompilationFailedException {
                InnerClassVisitor iv = new InnerClassVisitor(CompilationUnit.this, source);
                iv.visitClass(classNode);
            }
        }, Phases.SEMANTIC_ANALYSIS);
        addPhaseOperation(new PrimaryClassNodeOperation() {
            @Override
            public void call(SourceUnit source, GeneratorContext context,
                             ClassNode classNode) throws CompilationFailedException {
                if (!classNode.isSynthetic()) {
                    GenericsVisitor genericsVisitor = new GenericsVisitor(source);
                    genericsVisitor.visitClass(classNode);
                }
            }
        }, Phases.SEMANTIC_ANALYSIS);
        addPhaseOperation(new PrimaryClassNodeOperation() {
            @Override
            public void call(SourceUnit source, GeneratorContext context,
                             ClassNode classNode) throws CompilationFailedException {
                TraitComposer.doExtendTraits(classNode, source, CompilationUnit.this);
            }
        }, Phases.CANONICALIZATION);
        addPhaseOperation(compileCompleteCheck, Phases.CANONICALIZATION);
        addPhaseOperation(classgen, Phases.CLASS_GENERATION);

        addPhaseOperation(output);

        addPhaseOperation(new PrimaryClassNodeOperation() {
            @Override
            public void call(SourceUnit source, GeneratorContext context,
                             ClassNode classNode) throws CompilationFailedException {
                AnnotationCollectorTransform.ClassChanger actt = new AnnotationCollectorTransform.ClassChanger();
                actt.transformClass(classNode);
            }
        }, Phases.SEMANTIC_ANALYSIS);
        ASTTransformationVisitor.addPhaseOperations(this);
        addPhaseOperation(new PrimaryClassNodeOperation() {
            @Override
            public void call(SourceUnit source, GeneratorContext context,
                             ClassNode classNode) throws CompilationFailedException {
                StaticVerifier sv = new StaticVerifier();
                sv.visitClass(classNode, source);
            }
        }, Phases.SEMANTIC_ANALYSIS);
        addPhaseOperation(new PrimaryClassNodeOperation() {
            @Override
            public void call(SourceUnit source, GeneratorContext context,
                             ClassNode classNode) throws CompilationFailedException {
                InnerClassCompletionVisitor iv = new InnerClassCompletionVisitor(CompilationUnit.this, source);
                iv.visitClass(classNode);
            }
        }, Phases.CANONICALIZATION);
        addPhaseOperation(new PrimaryClassNodeOperation() {
            @Override
            public void call(SourceUnit source, GeneratorContext context,
                             ClassNode classNode) throws CompilationFailedException {
                EnumCompletionVisitor ecv = new EnumCompletionVisitor(CompilationUnit.this, source);
                ecv.visitClass(classNode);
            }
        }, Phases.CANONICALIZATION);
        addPhaseOperation(new PrimaryClassNodeOperation() {
            @Override
            public void call(SourceUnit source, GeneratorContext context,
                             ClassNode classNode) throws CompilationFailedException {
                Object callback = classNode.getNodeMetaData(StaticCompilationMetadataKeys.DYNAMIC_OUTER_NODE_CALLBACK);
                if (callback instanceof PrimaryClassNodeOperation) {
                    ((PrimaryClassNodeOperation) callback).call(source, context, classNode);
                    classNode.removeNodeMetaData(StaticCompilationMetadataKeys.DYNAMIC_OUTER_NODE_CALLBACK);
                }
            }
        }, Phases.INSTRUCTION_SELECTION);
    }

    private void applyCompilationCustomizers() {
        for (CompilationCustomizer customizer : configuration.getCompilationCustomizers()) {
            if (customizer instanceof CompilationUnitAware) {
                ((CompilationUnitAware) customizer).setCompilationUnit(this);
            }
            addPhaseOperation(customizer, customizer.getPhase().getPhaseNumber());
        }
    }

    /**
     * Returns the class loader for loading AST transformations.
     * @return - the transform class loader
     */
    public GroovyClassLoader getTransformLoader() {
        return astTransformationsContext.getTransformLoader() == null ? getClassLoader() : astTransformationsContext.getTransformLoader();
    }

    public void addPhaseOperation(SourceUnitOperation op, int phase) {
        validatePhase(phase);
        phaseOperations[phase].add(op);
    }

    public void addPhaseOperation(PrimaryClassNodeOperation op, int phase) {
        validatePhase(phase);
        phaseOperations[phase].add(op);
    }

    private static void validatePhase(int phase) {
        if (phase < 0 || phase > Phases.ALL) throw new IllegalArgumentException("phase " + phase + " is unknown");
    }

    public void addFirstPhaseOperation(PrimaryClassNodeOperation op, int phase) {
        validatePhase(phase);
        phaseOperations[phase].add(0, op);
    }

    public void addPhaseOperation(GroovyClassOperation op) {
        phaseOperations[Phases.OUTPUT].addFirst(op);
    }

    public void addNewPhaseOperation(SourceUnitOperation op, int phase) {
        validatePhase(phase);
        newPhaseOperations[phase].add(op);
    }

    /**
     * Configures its debugging mode and classloader classpath from a given compiler configuration.
     * This cannot be done more than once due to limitations in {@link java.net.URLClassLoader URLClassLoader}.
     */
    @Override
    public void configure(CompilerConfiguration configuration) {
        super.configure(configuration);
        this.debug = this.configuration.getDebug();
        this.configured = true;
    }

    /**
     * Returns the CompileUnit that roots our AST.
     */
    public CompileUnit getAST() {
        return this.ast;
    }

    /**
     * Get the source summaries
     */
    public Map getSummariesBySourceName() {
        return summariesBySourceName;
    }

    public Map getSummariesByPublicClassName() {
        return summariesByPublicClassName;
    }

    public Map getClassSourcesByPublicClassName() {
        return classSourcesByPublicClassName;
    }

    public boolean isPublicClass(String className) {
        return summariesByPublicClassName.containsKey(className);
    }

    /**
     * Get the GroovyClasses generated by compile().
     */
    public List<GroovyClass> getClasses() {
        return generatedClasses;
    }

    /**
     * Convenience routine to get the first ClassNode, for
     * when you are sure there is only one.
     */
    public ClassNode getFirstClassNode() {
        return this.ast.getModules().get(0).getClasses().get(0);
    }

    /**
     * Convenience routine to get the named ClassNode.
     */
    public ClassNode getClassNode(final String name) {
        final ClassNode[] result = new ClassNode[1];
        PrimaryClassNodeOperation handler = new PrimaryClassNodeOperation() {
            @Override
            public void call(SourceUnit source, GeneratorContext context, ClassNode classNode) {
                if (classNode.getName().equals(name)) {
                    result[0] = classNode;
                }
            }
        };

        try {
            applyToPrimaryClassNodes(handler);
        } catch (CompilationFailedException e) {
            if (debug) e.printStackTrace();
        }
        return result[0];
    }

    /**
     * @return the AST transformations current context
     */
    public ASTTransformationsContext getASTTransformationsContext() {
        return astTransformationsContext;
    }

    //---------------------------------------------------------------------------
    // SOURCE CREATION

    /**
     * Adds a set of file paths to the unit.
     */
    public void addSources(String[] paths) {
        for (String path : paths) {
            addSource(new File(path));
        }
    }

    /**
     * Adds a set of source files to the unit.
     */
    public void addSources(File[] files) {
        for (File file : files) {
            addSource(file);
        }
    }

    /**
     * Adds a source file to the unit.
     */
    public SourceUnit addSource(File file) {
        return addSource(new SourceUnit(file, configuration, classLoader, getErrorCollector()));
    }

    /**
     * Adds a source file to the unit.
     */
    public SourceUnit addSource(URL url) {
        return addSource(new SourceUnit(url, configuration, classLoader, getErrorCollector()));
    }

    /**
     * Adds a InputStream source to the unit.
     */
    public SourceUnit addSource(String name, InputStream stream) {
        ReaderSource source = new InputStreamReaderSource(stream, configuration);
        return addSource(new SourceUnit(name, source, configuration, classLoader, getErrorCollector()));
    }

    public SourceUnit addSource(String name, String scriptText) {
        return addSource(new SourceUnit(name, scriptText, configuration, classLoader, getErrorCollector()));
    }

    /**
     * Adds a SourceUnit to the unit.
     */
    public SourceUnit addSource(SourceUnit source) {
        String name = source.getName();
        source.setClassLoader(this.classLoader);
        for (SourceUnit su : queuedSources) {
            if (name.equals(su.getName())) return su;
        }
        queuedSources.add(source);
        return source;
    }

    /**
     * Returns an iterator on the unit's SourceUnits.
     */
    public Iterator<SourceUnit> iterator() {
        return new Iterator<SourceUnit>() {
            Iterator<String> nameIterator = sources.keySet().iterator();
            @Override
            public boolean hasNext() {
                return nameIterator.hasNext();
            }
            @Override
            public SourceUnit next() {
                String name = nameIterator.next();
                return sources.get(name);
            }
            @Override
            public void remove() {
                throw new UnsupportedOperationException();
            }
        };
    }

    /**
     * Adds a ClassNode directly to the unit (ie. without source).
     * WARNING: the source is needed for error reporting, using
     * this method without setting a SourceUnit will cause
     * NullPinterExceptions
     */
    public void addClassNode(ClassNode node) {
        ModuleNode module = new ModuleNode(this.ast);
        this.ast.addModule(module);
        module.addClass(node);
    }

    //---------------------------------------------------------------------------
    // EXTERNAL CALLBACKS

    /**
     * A callback interface you can use to "accompany" the classgen()
     * code as it traverses the ClassNode tree.  You will be called-back
     * for each primary and inner class.  Use setClassgenCallback() before
     * running compile() to set your callback.
     */
    // TODO: Convert to functional interface?
    public abstract static class ClassgenCallback {
        public abstract void call(ClassVisitor writer, ClassNode node) throws CompilationFailedException;
    }

    /**
     * Sets a ClassgenCallback.  You can have only one, and setting
     * it to {@code null} removes any existing setting.
     */
    public void setClassgenCallback(ClassgenCallback visitor) {
        this.classgenCallback = visitor;
    }

    public ClassgenCallback getClassgenCallback() {
        return classgenCallback;
    }

    /**
     * A callback interface you can use to get a callback after every
     * unit of the compile process.  You will be called-back with a
     * ProcessingUnit and a phase indicator.  Use setProgressCallback()
     * before running compile() to set your callback.
     */
    // TODO: Convert to functional interface?
    public abstract static class ProgressCallback {
        public abstract void call(ProcessingUnit context, int phase) throws CompilationFailedException;
    }

    /**
     * Sets a ProgressCallback.  You can have only one, and setting
     * it to {@code null} removes any existing setting.
     */
    public void setProgressCallback(ProgressCallback callback) {
        this.progressCallback = callback;
    }

    public ProgressCallback getProgressCallback() {
        return progressCallback;
    }

    //---------------------------------------------------------------------------
    // ACTIONS

    /**
     * Synonym for compile(Phases.ALL).
     */
    public void compile() throws CompilationFailedException {
        compile(Phases.ALL);
    }

    /**
     * Compiles the compilation unit from sources.
     */
    public void compile(int throughPhase) throws CompilationFailedException {
        //
        // To support delta compilations, we always restart
        // the compiler.  The individual passes are responsible
        // for not reprocessing old code.
        gotoPhase(Phases.INITIALIZATION);
        throughPhase = Math.min(throughPhase, Phases.ALL);

        while (throughPhase >= phase && phase <= Phases.ALL) {

            if (phase == Phases.SEMANTIC_ANALYSIS) {
                doPhaseOperation(resolve);
                if (dequeued()) continue;
            }

            processPhaseOperations(phase);
            // Grab processing may have brought in new AST transforms into various phases, process them as well
            processNewPhaseOperations(phase);

            if (progressCallback != null) progressCallback.call(this, phase);
            completePhase();
            applyToSourceUnits(mark);

            if (dequeued()) continue;

            gotoPhase(phase + 1);

            if (phase == Phases.CLASS_GENERATION) {
                sortClasses();
            }
        }

        errorCollector.failIfErrors();
    }

    private void processPhaseOperations(int ph) {
        LinkedList ops = phaseOperations[ph];
        for (Object next : ops) {
            doPhaseOperation(next);
        }
    }

    private void processNewPhaseOperations(int currPhase) {
        recordPhaseOpsInAllOtherPhases(currPhase);
        LinkedList currentPhaseNewOps = newPhaseOperations[currPhase];
        while (!currentPhaseNewOps.isEmpty()) {
            Object operation = currentPhaseNewOps.removeFirst();
            // push this operation to master list and then process it.
            phaseOperations[currPhase].add(operation);
            doPhaseOperation(operation);
            // if this operation has brought in more phase ops for ast transforms, keep recording them
            // in master list of other phases and keep processing them for this phase.
            recordPhaseOpsInAllOtherPhases(currPhase);
            currentPhaseNewOps = newPhaseOperations[currPhase];
        }
    }

    private void doPhaseOperation(Object operation) {
        if (operation instanceof PrimaryClassNodeOperation) {
            applyToPrimaryClassNodes((PrimaryClassNodeOperation) operation);
        } else if (operation instanceof SourceUnitOperation) {
            applyToSourceUnits((SourceUnitOperation) operation);
        } else {
            applyToGeneratedGroovyClasses((GroovyClassOperation) operation);
        }
    }

    private void recordPhaseOpsInAllOtherPhases(int currPhase) {
        // apart from current phase, push new operations for every other phase in the master phase ops list
        for (int ph = Phases.INITIALIZATION; ph <= Phases.ALL; ph++) {
            if (ph != currPhase && !newPhaseOperations[ph].isEmpty()) {
                phaseOperations[ph].addAll(newPhaseOperations[ph]);
                newPhaseOperations[ph].clear();
            }
        }
    }

    private void sortClasses() throws CompilationFailedException {
        for (ModuleNode module : this.ast.getModules()) {
            module.sortClasses();
        }
    }

    /**
     * Dequeues any source units add through addSource and resets the compiler phase
     * to initialization.
     * <p>
     * Note: this does not mean a file is recompiled. If a SourceUnit has already passed
     * a phase it is skipped until a higher phase is reached.
     *
     * @return true if there was a queued source
     * @throws CompilationFailedException
     */
    protected boolean dequeued() throws CompilationFailedException {
        boolean dequeue = !queuedSources.isEmpty();
        while (!queuedSources.isEmpty()) {
            SourceUnit su = queuedSources.removeFirst();
            String name = su.getName();
            sources.put(name, su);
        }
        if (dequeue) {
            gotoPhase(Phases.INITIALIZATION);
        }
        return dequeue;
    }

    /**
     * Resolves all types
     */
    private final SourceUnitOperation resolve = new SourceUnitOperation() {
        public void call(SourceUnit source) throws CompilationFailedException {
            List<ClassNode> classes = source.ast.getClasses();
            for (ClassNode node : classes) {
                VariableScopeVisitor scopeVisitor = new VariableScopeVisitor(source);
                scopeVisitor.visitClass(node);

                resolveVisitor.setClassNodeResolver(classNodeResolver);
                resolveVisitor.startResolving(node, source);
            }

        }
    };

    private final PrimaryClassNodeOperation staticImport = new PrimaryClassNodeOperation() {
        public void call(SourceUnit source, GeneratorContext context, ClassNode classNode) throws CompilationFailedException {
            staticImportVisitor.visitClass(classNode, source);
        }
    };

    /**
     * Runs convert() on a single SourceUnit.
     */
    private final SourceUnitOperation convert = new SourceUnitOperation() {
        public void call(SourceUnit source) throws CompilationFailedException {
            source.convert();
            CompilationUnit.this.ast.addModule(source.getAST());


            if (CompilationUnit.this.progressCallback != null) {
                CompilationUnit.this.progressCallback.call(source, CompilationUnit.this.phase);
            }
        }
    };

    private final GroovyClassOperation output = new GroovyClassOperation() {
        public void call(GroovyClass gclass) throws CompilationFailedException {
            String name = gclass.getName().replace('.', File.separatorChar) + ".class";
            File path = new File(configuration.getTargetDirectory(), name);

            //
            // Ensure the path is ready for the file
            //
            File directory = path.getParentFile();
            if (directory != null && !directory.exists()) {
                directory.mkdirs();
            }

            //
            // Create the file and write out the data
            //
            byte[] bytes = gclass.getBytes();

            try (FileOutputStream stream = new FileOutputStream(path)) {
                stream.write(bytes, 0, bytes.length);
            } catch (IOException e) {
                getErrorCollector().addError(Message.create(e.getMessage(), CompilationUnit.this));
            }
        }
    };

    /* checks if all needed classes are compiled before generating the bytecode */
    private final SourceUnitOperation compileCompleteCheck = new SourceUnitOperation() {
        public void call(SourceUnit source) throws CompilationFailedException {
            List<ClassNode> classes = source.ast.getClasses();
            for (ClassNode node : classes) {
                CompileUnit cu = node.getCompileUnit();
                for (Iterator<String> iter = cu.iterateClassNodeToCompile(); iter.hasNext();) {
                    String name = iter.next();
                    SourceUnit su = ast.getScriptSourceLocation(name);
                    List<ClassNode> classesInSourceUnit = su.ast.getClasses();
                    StringBuilder message = new StringBuilder();
                    message
                            .append("Compilation incomplete: expected to find the class ")
                            .append(name)
                            .append(" in ")
                            .append(su.getName());
                    if (classesInSourceUnit.isEmpty()) {
                        message.append(", but the file seems not to contain any classes");
                    } else {
                        message.append(", but the file contains the classes: ");
                        boolean first = true;
                        for (ClassNode cn : classesInSourceUnit) {
                            if (!first) {
                                message.append(", ");
                            } else {
                                first = false;
                            }
                            message.append(cn.getName());
                        }
                    }

                    getErrorCollector().addErrorAndContinue(
                            new SimpleMessage(message.toString(), CompilationUnit.this)
                    );
                    iter.remove();
                }
            }
        }
    };

    /**
     * Runs classgen() on a single ClassNode.
     */
    private final PrimaryClassNodeOperation classgen = new PrimaryClassNodeOperation() {
        @Override
        public boolean needSortedInput() {
            return true;
        }
        @Override
        public void call(SourceUnit source, GeneratorContext context, ClassNode classNode) throws CompilationFailedException {
            optimizer.visitClass(classNode, source); // GROOVY-4272: repositioned it here from staticImport

            //
            // Run the Verifier on the outer class
            //
            try {
                verifier.visitClass(classNode);
            } catch (GroovyRuntimeException rpe) {
                ASTNode node = rpe.getNode();
                getErrorCollector().addError(
                        new SyntaxException(rpe.getMessage(), node.getLineNumber(), node.getColumnNumber(), node.getLastLineNumber(), node.getLastColumnNumber()),
                        source
                );
            }

            LabelVerifier lv = new LabelVerifier(source);
            lv.visitClass(classNode);

            ClassCompletionVerifier completionVerifier = new ClassCompletionVerifier(source);
            completionVerifier.visitClass(classNode);

            ExtendedVerifier xverifier = new ExtendedVerifier(source);
            xverifier.visitClass(classNode);

            // because the class may be generated even if a error was found
            // and that class may have an invalid format we fail here if needed
            getErrorCollector().failIfErrors();

            //
            // Prep the generator machinery
            //
            ClassVisitor visitor = createClassVisitor();

            String sourceName = (source == null ? classNode.getModule().getDescription() : source.getName());
            // only show the file name and its extension like javac does in its stacktraces rather than the full path
            // also takes care of both \ and / depending on the host compiling environment
            if (sourceName != null)
                sourceName = sourceName.substring(Math.max(sourceName.lastIndexOf('\\'), sourceName.lastIndexOf('/')) + 1);
            AsmClassGenerator generator = new AsmClassGenerator(source, context, visitor, sourceName);

            //
            // Run the generation and create the class (if required)
            //
            generator.visitClass(classNode);

            byte[] bytes = ((ClassWriter) visitor).toByteArray();
            generatedClasses.add(new GroovyClass(classNode.getName(), bytes));

            //
            // Handle any callback that's been set
            //
            if (CompilationUnit.this.classgenCallback != null) {
                classgenCallback.call(visitor, classNode);
            }

            //
            // Recurse for inner classes
            //
            LinkedList<ClassNode> innerClasses = generator.getInnerClasses();
            while (!innerClasses.isEmpty()) {
                classgen.call(source, context, innerClasses.removeFirst());
            }
        }
    };

    protected ClassVisitor createClassVisitor() {
        CompilerConfiguration config = getConfiguration();
        int computeMaxStackAndFrames = ClassWriter.COMPUTE_MAXS;
        if (CompilerConfiguration.isPostJDK7(config.getTargetBytecode()) || config.isIndyEnabled()) {
            computeMaxStackAndFrames += ClassWriter.COMPUTE_FRAMES;
        }
        return new ClassWriter(computeMaxStackAndFrames) {
            private ClassNode getClassNode(String name) {
                // try classes under compilation
                CompileUnit cu = getAST();
                ClassNode cn = cu.getClass(name);
                if (cn!=null) return cn;
                // try inner classes
                cn = cu.getGeneratedInnerClass(name);
                if (cn!=null) return cn;
                ClassNodeResolver.LookupResult lookupResult = getClassNodeResolver().resolveName(name, CompilationUnit.this);
                return lookupResult == null ? null : lookupResult.getClassNode();
            }
            private ClassNode getCommonSuperClassNode(ClassNode c, ClassNode d) {
                // adapted from ClassWriter code
                if (c.isDerivedFrom(d)) return d;
                if (d.isDerivedFrom(c)) return c;
                if (c.isInterface() || d.isInterface()) return ClassHelper.OBJECT_TYPE;
                do {
                    c = c.getSuperClass();
                } while (c!=null && !d.isDerivedFrom(c));
                if (c==null) return ClassHelper.OBJECT_TYPE;
                return c;
            }
            @Override
            protected String getCommonSuperClass(String arg1, String arg2) {
                ClassNode a = getClassNode(arg1.replace('/', '.'));
                ClassNode b = getClassNode(arg2.replace('/', '.'));
                return getCommonSuperClassNode(a,b).getName().replace('.','/');
            }
        };
    }

    //---------------------------------------------------------------------------
    // PHASE HANDLING

    /**
     * Updates the phase marker on all sources.
     */
    protected void mark() throws CompilationFailedException {
        applyToSourceUnits(mark);
    }

    /**
     * Marks a single SourceUnit with the current phase,
     * if it isn't already there yet.
     */
    private final SourceUnitOperation mark = new SourceUnitOperation() {
        public void call(SourceUnit source) throws CompilationFailedException {
            if (source.phase < phase) {
                source.gotoPhase(phase);
            }

            if (source.phase == phase && phaseComplete && !source.phaseComplete) {
                source.completePhase();
            }
        }
    };

    //---------------------------------------------------------------------------
    // LOOP SIMPLIFICATION FOR SourceUnit OPERATIONS

    /**
     * An callback interface for use in the applyToSourceUnits loop driver.
     */
    public abstract static class SourceUnitOperation {
        public abstract void call(SourceUnit source) throws CompilationFailedException;
    }

    /**
     * A loop driver for applying operations to all SourceUnits.
     * Automatically skips units that have already been processed
     * through the current phase.
     */
    public void applyToSourceUnits(SourceUnitOperation body) throws CompilationFailedException {
        for (String name : sources.keySet()) {
            SourceUnit source = sources.get(name);
            if ((source.phase < phase) || (source.phase == phase && !source.phaseComplete)) {
                try {
                    body.call(source);
                } catch (CompilationFailedException e) {
                    throw e;
                } catch (Exception e) {
                    GroovyBugError gbe = new GroovyBugError(e);
                    changeBugText(gbe, source);
                    throw gbe;
                } catch (GroovyBugError e) {
                    changeBugText(e, source);
                    throw e;
                }
            }
        }

        getErrorCollector().failIfErrors();
    }

    //---------------------------------------------------------------------------
    // LOOP SIMPLIFICATION FOR PRIMARY ClassNode OPERATIONS

    /**
     * An callback interface for use in the applyToPrimaryClassNodes loop driver.
     */
    public abstract static class PrimaryClassNodeOperation {
        public abstract void call(SourceUnit source, GeneratorContext context, ClassNode classNode) throws CompilationFailedException;

        public boolean needSortedInput() {
            return false;
        }
    }

    public abstract static class GroovyClassOperation {
        public abstract void call(GroovyClass gclass) throws CompilationFailedException;
    }

    private static int getSuperClassCount(ClassNode element) {
        int count = 0;
        while (element != null) {
            count++;
            element = element.getSuperClass();
        }
        return count;
    }

    private int getSuperInterfaceCount(ClassNode element) {
        int count = 1;
        ClassNode[] interfaces = element.getInterfaces();
        for (ClassNode anInterface : interfaces) {
            count = Math.max(count, getSuperInterfaceCount(anInterface) + 1);
        }
        return count;
    }

    private List<ClassNode> getPrimaryClassNodes(boolean sort) {
        List<ClassNode> unsorted = new ArrayList<ClassNode>();
        for (ModuleNode module : this.ast.getModules()) {
            unsorted.addAll(module.getClasses());
        }

        if (!sort) return unsorted;

        int unsortedSize = unsorted.size();
        int[] indexClass = new int[unsortedSize];
        int[] indexInterface = new int[unsortedSize];
        {
            int i = 0;
            for (Iterator<ClassNode> iter = unsorted.iterator(); iter.hasNext(); i++) {
                ClassNode element = iter.next();
                if (element.isInterface()) {
                    indexInterface[i] = getSuperInterfaceCount(element);
                    indexClass[i] = -1;
                } else {
                    indexClass[i] = getSuperClassCount(element);
                    indexInterface[i] = -1;
                }
            }
        }

        List<ClassNode> sorted = getSorted(indexInterface, unsorted);
        sorted.addAll(getSorted(indexClass, unsorted));
        return sorted;
    }

    private static List<ClassNode> getSorted(int[] index, List<ClassNode> unsorted) {
        int unsortedSize = unsorted.size();
        List<ClassNode> sorted = new ArrayList<ClassNode>(unsortedSize);
        for (int i = 0; i < unsortedSize; i++) {
            int min = -1;
            for (int j = 0; j < unsortedSize; j++) {
                if (index[j] == -1) continue;
                if (min == -1 || index[j] < index[min]) {
                    min = j;
                }
            }
            if (min == -1) break;
            sorted.add(unsorted.get(min));
            index[min] = -1;
        }
        return sorted;
    }

    /**
     * A loop driver for applying operations to all primary ClassNodes in
     * our AST.  Automatically skips units that have already been processed
     * through the current phase.
     */
    public void applyToPrimaryClassNodes(PrimaryClassNodeOperation body) throws CompilationFailedException {
        for (ClassNode classNode : getPrimaryClassNodes(body.needSortedInput())) {
            SourceUnit context = null;
            try {
                context = classNode.getModule().getContext();
                if (context == null || context.phase < phase || (context.phase == phase && !context.phaseComplete)) {
                    int offset = 1;
                    for (Iterator<InnerClassNode> iterator = classNode.getInnerClasses(); iterator.hasNext(); ) {
                        iterator.next();
                        offset++;
                    }
                    body.call(context, new GeneratorContext(this.ast, offset), classNode);
                }
            } catch (CompilationFailedException e) {
                // fall through, getErrorReporter().failIfErrors() will trigger
            } catch (NullPointerException npe) {
                GroovyBugError gbe = new GroovyBugError("unexpected NullPointerException", npe);
                changeBugText(gbe, context);
                throw gbe;
            } catch (GroovyBugError e) {
                changeBugText(e, context);
                throw e;
            } catch (NoClassDefFoundError | Exception e) {
                // effort to get more logging in case a dependency of a class is loaded
                // although it shouldn't have
                convertUncaughtExceptionToCompilationError(e);
            }
        }

        getErrorCollector().failIfErrors();
    }

    private void convertUncaughtExceptionToCompilationError(final Throwable e) {
        // check the exception for a nested compilation exception
        ErrorCollector nestedCollector = null;
        for (Throwable next = e.getCause(); next != e && next != null; next = next.getCause()) {
            if (!(next instanceof MultipleCompilationErrorsException)) continue;
            MultipleCompilationErrorsException mcee = (MultipleCompilationErrorsException) next;
            nestedCollector = mcee.collector;
            break;
        }

        if (nestedCollector != null) {
            getErrorCollector().addCollectorContents(nestedCollector);
        } else {
            Exception err = e instanceof Exception?((Exception)e):new RuntimeException(e);
            getErrorCollector().addError(new ExceptionMessage(err, configuration.getDebug(), this));
        }
    }

    public void applyToGeneratedGroovyClasses(GroovyClassOperation body) throws CompilationFailedException {
        if (this.phase != Phases.OUTPUT && !(this.phase == Phases.CLASS_GENERATION && this.phaseComplete)) {
            throw new GroovyBugError("CompilationUnit not ready for output(). Current phase=" + getPhaseDescription());
        }

        for (GroovyClass gclass : this.generatedClasses) {
            //
            // Get the class and calculate its filesystem name
            //
            try {
                body.call(gclass);
            } catch (CompilationFailedException e) {
                // fall through, getErrorReporter().failIfErrors() will trigger
            } catch (NullPointerException npe) {
                throw npe;
            } catch (GroovyBugError e) {
                changeBugText(e, null);
                throw e;
            } catch (Exception e) {
                throw new GroovyBugError(e);
            }
        }

        getErrorCollector().failIfErrors();
    }

    private void changeBugText(GroovyBugError e, SourceUnit context) {
        e.setBugText("exception in phase '" + getPhaseDescription() + "' in source unit '" + ((context != null) ? context.getName() : "?") + "' " + e.getBugText());
    }

    public ClassNodeResolver getClassNodeResolver() {
        return classNodeResolver;
    }

    public void setClassNodeResolver(ClassNodeResolver classNodeResolver) {
        this.classNodeResolver = classNodeResolver;
    }

    public Set<JavaFileObject> getJavaCompilationUnitSet() {
        return javaCompilationUnitSet;
    }

    public void addJavaCompilationUnits(Set<JavaFileObject> javaCompilationUnitSet) {
        this.javaCompilationUnitSet.addAll(javaCompilationUnitSet);
    }
}
